Skip to content

EEPIC-#79 - v3 refactor: PSR-compliant client with typed DTOs, reposi…#153

Open
derpixler wants to merge 3 commits into
masterfrom
epic-79/pre-merge-develop
Open

EEPIC-#79 - v3 refactor: PSR-compliant client with typed DTOs, reposi…#153
derpixler wants to merge 3 commits into
masterfrom
epic-79/pre-merge-develop

Conversation

@derpixler

Copy link
Copy Markdown
Contributor

EEPIC-#79 — v3: PSR-compliant PHP client with typed DTOs, repositories, and enums

Breaking Changes (v2 → v3)

Architecture

v2 v3
Client (arrays, magic strings) ZammadClient with typed factory methods
Resource + ResourceType enums Repository pattern + typed DTOs
$ticket->getValue('title') $ticket->title (typed property, IDE autocomplete)
$ticket->hasError() / $ticket->getError() Typed exceptions (NotFoundException, etc.)
new Client(['url' => ..., 'http_token' => ...]) ZammadClient::withToken($url, $token)
PHP >= 7.2 PHP >= 8.1

Files removed (~3,900 LOC)

  • src/Client.php, src/HTTPClient.php, src/HTTPClientInterface.php
  • src/Resource/ — all 10 resource classes (AbstractResource, Ticket, User, etc.)
  • src/ResourceType.php
  • src/Exception/ — old exception classes
  • test/ZammadAPIClient/ — old test suite

What's new (~10,200 LOC)

Core layer (src/Core/)

  • ZammadClient — entry point with withToken(), withOAuth2(), withBasicAuth(),
    withClient(). Auto-normalizes URL (appends /api/v1). Memoized repositories via repo().
    Impersonation via performOnBehalfOf() / setOnBehalfOfUser().
  • RequestHandler — PSR-18 HTTP transport with JSON (de)serialization, HTTP status→exception
    mapping, X-On-Behalf-Of header support, configurable retries.
  • RetryAfterMiddleware — PSR-18 decorator: auto-retries on HTTP 429 with Respect-Retry-After
    header parsing (RFC 7231 HTTP-date + delta-seconds).
  • AbstractRepository — generic CRUD with generator-based cursor pagination, explicit page
    methods, resource()/list() Ruby-style accessors, extractItems().
  • PaginatedList — ArrayAccess + IteratorAggregate with page(), pageNext(), pagePrev(),
    each(), totalCount(), allPages().
  • Resource — mutable stateful wrapper with property-level change tracking.
    save() sends only changed fields; destroy() sends DELETE.
  • DtoHydrator — reflection-based hydration: maps API array keys to constructor parameters
    with type-driven coercion via Cast.
  • Cast — safe scalar coercions: dateTime(), string(), intOrNull(), boolOrNull().
  • ConnectionConfig — immutable config DTO (verifySsl, timeout, maxRetries, logger).
  • RepositoryRegistry — single source of truth mapping 10 repositories to API paths and DTOs.
  • ResponseParser — extracts resource arrays from Zammad's {tickets: [...], assets: {...}} wrapper.
  • HttpPageFetcher — PSR-18 page fetcher for PaginatedList.
  • Traits: HasTimestamps, HydratesFromArray, SerializesToArray.

Contracts (src/Core/Contracts/)

  • RepositoryInterface — find, all, search, create, update, patch, delete
  • DTOInterface — fromArray(), toArray(), id()
  • DeletableInterface — delete($id)
  • PatchableInterface — toPatchArray()
  • RequestHandlerInterface, ClientInterface, PageFetcherInterface

Endpoints — 10 repositories + typed DTOs (src/Endpoints/)

  • Tickets: TicketRepository (CRUD + delete + getTicketArticles),
    TicketDTO (14 fields), TicketUpdateDTO (partial update, 8 fields)
  • Users: UserRepository (CRUD + delete + CSV import), UserDTO (13 fields)
  • Organizations: OrganizationRepository (CRUD + delete + CSV import), OrganizationDTO
  • Groups: GroupRepository (CRUD + delete), GroupDTO
  • TicketArticles: TicketArticleRepository (getForTicket, getAttachmentContent),
    TicketArticleDTO (25 fields), TicketArticleType enum (Note, Email, Phone, Sms, Web)
  • TicketStates: TicketStateRepository, TicketStateDTO
  • TicketPriorities: TicketPriorityRepository, TicketPriorityDTO
  • Tags: TagRepository (add, remove, tagSearch), TagDTO
  • TextModules: TextModuleRepository (CRUD + delete + CSV import), TextModuleDTO
  • Links: LinkRepository (add, remove, list), LinkDTO

Exceptions (src/Exceptions/) — typed hierarchy

  • AuthenticationException (401), ForbiddenException (403), NotFoundException (404)
  • ValidationException (422, with $errors array per-field)
  • RateLimitException (429, with $retryAfterSeconds, auto-retried)
  • ServerErrorException (5xx), NetworkException (DNS, timeout, connection refused)

Bridge (src/Bridge/)

  • LaravelServiceProvider — auto-registers ZammadClient as singleton
  • SymfonyBundle — Symfony DI integration

Tests — 227 unit tests, 12 integration test classes

  • Unit: AbstractRepository, Cast, DtoHydrator, PaginatedList, RequestHandler, Resource,
    RepositoryRegistry, RetryAfterMiddleware, all DTOs, all repositories, ZammadClient
  • Integration: Tickets, Users, Groups, Organizations, Tags, Links, TicketArticles,
    Pagination, Search, ErrorHandling, ReferenceData, User cleanup

Documentation

  • docs/migration-v3.md — step-by-step v2→v3 migration guide
  • docs/migration-v3-examples.md — 15 side-by-side before/after code examples
  • docs/v2-reference.md — preserved v2 documentation
  • docs/alternative-clients.md — non-Guzzle transport setup
  • examples/cookbook.php — 9 runnable v3 recipes
  • README.md — rewritten with DTO field tables, update method decision guide,
    error handling table (HTTP→exception mapping + auto-retry), paradigm guide

CI

  • New GitHub Actions workflow: PSR-12 lint, PHPStan level=max, 227 unit tests,
    integration tests against live Zammad instance

@derpixler
derpixler force-pushed the epic-79/pre-merge-develop branch 10 times, most recently from ac65aee to d904081 Compare July 20, 2026 06:34
…tories, enums, PHP 8.1+

## Breaking Changes (v2 → v3)

### Architecture
- ZammadClient with typed factory methods replaces Client (arrays, magic strings)
- Repository pattern + typed DTOs replaces Resource + ResourceType enums
- Typed exceptions (NotFoundException, etc.) replaces hasError()/getError()
- PHP >= 8.1 (was PHP >= 7.2)

### Files removed (~3,900 LOC)
- src/Client.php, src/HTTPClient.php, src/HTTPClientInterface.php
- src/Resource/ — all 10 resource classes
- src/ResourceType.php, src/Exception/
- test/ZammadAPIClient/ — old test suite
- examples/ticket.php, examples/user.php, examples/v2-usage.php

## What's new (~10,200 LOC)

### Core (src/Core/)
- ZammadClient, RequestHandler, RetryAfterMiddleware, AbstractRepository,
  PaginatedList, Resource, DtoHydrator, Cast, ConnectionConfig,
  RepositoryRegistry, ResponseParser, HttpPageFetcher, traits

### Contracts (src/Core/Contracts/)
- RepositoryInterface, DTOInterface, DeletableInterface, PatchableInterface,
  RequestHandlerInterface, ClientInterface, PageFetcherInterface

### Endpoints — 10 repositories + typed DTOs (src/Endpoints/)
- Tickets (CRUD + delete + getTicketArticles), TicketDTO, TicketUpdateDTO
- Users (CRUD + delete + CSV import), UserDTO
- Organizations (CRUD + delete + CSV import), OrganizationDTO
- Groups (CRUD + delete), GroupDTO
- TicketArticles (getForTicket, getAttachmentContent), TicketArticleDTO,
  TicketArticleType enum (Note, Email, Phone, Sms, Web)
- TicketStates (read-only), TicketStateDTO
- TicketPriorities (read-only), TicketPriorityDTO
- Tags (add, remove, tagSearch), TagDTO
- TextModules (CRUD + delete + CSV import), TextModuleDTO
- Links (add, remove, list), LinkDTO

### Exceptions — typed hierarchy (src/Exceptions/)
- AuthenticationException (401), ForbiddenException (403),
  NotFoundException (404), ValidationException (422, with $errors array),
  RateLimitException (429, auto-retried), ServerErrorException (5xx),
  NetworkException

### Bridge (src/Bridge/)
- LaravelServiceProvider, SymfonyBundle

### Tests — 227 unit tests, 12 integration test classes

### Documentation
- docs/migration-v3.md, docs/migration-v3-examples.md (15 side-by-side examples)
- docs/v2-reference.md, docs/alternative-clients.md
- examples/cookbook.php (9 runnable v3 recipes)
- README.md with DTO field tables, update decision guide, error mapping,
  paradigm guide, delete() availability table
@derpixler
derpixler force-pushed the epic-79/pre-merge-develop branch from d904081 to 83c29f2 Compare July 20, 2026 07:37
…rsonationHandler, slim client)

Split the monolithic ZammadClient into focused classes:

- ClientFactory → GuzzleClientFactory implements ClientFactoryInterface
  Guzzle wiring lives exclusively in GuzzleClientFactory::buildClient()
  Non-Guzzle via new ZammadClient(new RequestHandler(...))

- ImpersonationHandler — stateless decorator implementing
  RequestHandlerInterface. Injects From header on every request
  including getRaw(). No shared mutable state.

- ZammadClient — slimmed from ~210 to ~79 lines. Only repo() and
  getHandler(). Repository access via typed, explicit, IDE-friendly
  methods (ticket(), user(), group(), etc.) implemented directly on
  the class.

Removals and cleanups:

- __call, aliasMap, resolveAlias — replaced by explicit typed methods
- RequestHandler:: — shared mutable state removed
- RequestHandlerInterface::setOnBehalfOfUser/getOnBehalfOfUser — removed
- RequestHandlerInterface::getRaw() extended with $headers parameter
  for ImpersonationHandler compatibility
- onBehalfOf() / performOnBehalfOf() — not a Client concern;
  use new ZammadClient(new ImpersonationHandler($handler, $userId))
- getListKey() default $this->resourcePath; 10 identical impls removed

Namespace structure:

  Core/Contracts/   — ClientInterface, ClientFactoryInterface
  Core/Repository/  — AbstractRepository, RepositoryRegistry, PaginatedList, Resource, ResponseParser, DtoHydrator
  Core/Transport/   — RequestHandler, RetryAfterMiddleware, ImpersonationHandler, HttpPageFetcher
  Core/Traits/      — RepositoryAccessors (opt-in for custom ClientInterface impls), HasTimestamps, HydratesFromArray, SerializesToArray
  Factory/          — GuzzleClientFactory

Bug fixes:

- HttpPageFetcher::extractIndexResults() now reads total_count from
  API response instead of hard-coding null
- Delete monolithic examples/cookbook.php
- 00-plain.php: Guzzle setup, copy-paste-ready
- 00-laravel.php: Laravel service container reference
- 00-symfony.php: Symfony bundle reference
- 00-slim.php: Non-Guzzle setup (Symfony HttpClient + Nyholm)
- 01-quick-start.php: client setup + find()
- 02-crud.php: create, read, delete, error handling
- 03-listing.php: all() streaming, list() pagination, totalCount()
- 04-updates.php: patch(), TicketUpdateDTO, Resource wrapper
- 05-impersonation.php: ImpersonationHandler decoration
- 06-search.php: search(), searchList(), pagination
- CookbookIntegrationTest: executes recipes 01-06 via exec()
- README.md: recipe overview and run instructions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant